<?php
/**
 * MultiKeyRecord
 *
 * Класс MultiKeyRecord является базовым классом для всех объектов с мульти-ключами
 *
 * @author Кузьма Дмитрий <kouzma.d@gmail.com>
 * @version 1.1
 */
class MultiKeyRecord {
  
  
	public $tableFields = array ();  // Массив загруженных в объект полей
  protected $tableName; 					 // Название таблицы в БД
  protected $arrPkNames = array(); // Массив первичных ключей в таблице
  private $isLoaded = false;       // Флаг, обозначающий, была ли загружена связь из БД

	private $stmt;

	private $select = '*';							//Образующийся сковый запрос
	private $conditions = null;					//Условия поиска (то, что пишется после WHERE)
	private $order = null;							//Условия сортировки - то, что пишется после ORDER BY
	private $group = null;							//Условия группировки - то, что пишется после GROUP BY
	private $limit = null;							//Ограничение на получение элементов в поиске
	private $offset = 0;								//Позиция, начиная с которой следует выдавать поисковый результат
	private $include = array();					//Массив. Служит для добавления JOIN`ов к запросу
	private $includeList = array();			//Массив. Позволяет добавить список элементов для загрузки из БД. Например, амплуа для видов спорта array('player_role'). Используется для поиска одного элемента (можно прямо в конструкторе)
	private $combine = array();					//Массив. Позволяет настроить выборку так, что будут подключены доп. таблицы, а результат будет возвращен в виде многомерного маасива
																			//Имеет вид ('таблица' => 'поле1[, поле2]'[, 'таблица' => 'поле1[, поле2]'])
																			//'combine' => array('sport' => 'ID_sport, sport_name', 'user' => 'login, ID_user')
	private $joins = null;							//Строка. Позволяет вручную вставить подстроку JOIN ...
	private $getAuthor; 								//Для автоматического получения данных об авторе
	private $combineConverted = array();//Многомерный массив. Представляет собой массив $combine, в котором строки представлены массивами
  
  
  /**
   * Конструктор - ведет себя по разному, в зависимости от аргументов
   * если передан массив, то сохраняет его в tableFields
   * если передано n параметров, то записывает первые "k" из них в ключи массива и в tableFields. И загружает из БД автоматически объект
   * чтобы отменить автоматическую загрузку из БД необходимо передать "k+1" аргумент (любого содержимого, например, true)  
   */
	public function __construct() {
    Service_Debug::setDebug();
    $args = func_get_args();
		if ($args) { // Если аргументы переданы
		  if (is_array($args[0])) { // Если нам передали массив
			  $this->tableFields = &$args[0];
	  	  // Назначение PKs
	  	  foreach ($this->arrPkNames as $fieldName => $attrName) {
	  	  	$this->$attrName = $this->getField($fieldName);
	  	  }
		  }
		  else {
		    $i = 0;
		  	// Назначение PKs
	  	  foreach ($this->arrPkNames as $fieldName => $attrName) {
	  	  	$this->$attrName = intval($args[$i]);
	  	  	$this->setField($fieldName, $this->$attrName);
	  	  	$i++;
	  	  	if (!isset($args[$i])) break;
	  	  }
	  	  if (!isset($args[sizeof($this->arrPkNames)])) $this->_getOne();
		  }
		}
	}
	
  /**
	 * Получаем элемент класса MultyKeyRecord
	 * 
	 * @return array
	 */
	private function _getOne() {
		Service_Debug::setDebug ();
		$objDB = Service_Database::instance (); //Получаем подключение к БД
		$conditions = $this->_getKeysConditions();
		if (!$conditions) {
			$this->tableFields = array();
  		$this->isLoaded = false;
  		return $this->tableFields;
		}
		$stmt = "SELECT `" . $this->tableName . "`.* FROM `" . $this->tableName . "`
  	WHERE " . $conditions;
		$this->tableFields = $objDB->selectOne( $stmt );
		if ($this->tableFields) $this->isLoaded = true;
		else $this->isLoaded = false;
		return $this->tableFields;
	}
	
	/**
	 * Метод устанавливает значение поля
	 *
	 * @param string $fieldName - название поля
	 * @param mixed $fieldValue - значение поля
	 */
	public function setField($fieldName, $fieldValue) {
		Service_Debug::setDebug();
		$this->tableFields [$fieldName] = $fieldValue;
	}
	/**
	 * Метод устанавливает значение всех полей объекта, согласно переданному массиву
	 *
	 * @param array $tableFields
	 */
	public function setFields($tableFields, $reload = false) {
		Service_Debug::setDebug();
		if ($reload) $this->tableFields = &$tableFields;
		else foreach ( $tableFields as $key => $value ) $this->tableFields [$key] = $value;
	}
	/**
	 * Метод возвращает значение поля
	 *
	 * @param string $fieldName
	 * @return mixed
	 */
	public function getField($fieldName) {
		Service_Debug::setDebug();
		if (isset ( $this->tableFields [$fieldName] )) return $this->tableFields [$fieldName];
		else return null;
	}
	
  /**
   * Возвращает флаг загруженности объекта из БД
   *
   */
  public function isLoaded() {
    Service_Debug::setDebug();
  	return $this->isLoaded;
  }
  
	/**
	 * Метод производит непосредственное сохранение объекта
	 *
	 * @return integer
	 */
	public function save() {
		Service_Debug::setDebug ();
		$objDB = Service_Database::instance (); //Получаем подключение к БД
		
		if (!$this->isLoaded) {
			if (!$this->tableFields) {
				foreach ($this->arrPkNames as $fieldName => $attrName) {
					$this->setField($fieldName, $this->$attrName);
				}
			}
			$retValue = $objDB->insert( $this->tableName, $this->tableFields );
			if ($retValue) {
				foreach ($this->arrPkNames as $fieldName => $attrName) {
					$this->$attrName = $this->getField($fieldName);
				}
				$this->isLoaded = true;
			}
		}
		else $retValue = $objDB->update ( $this->tableName, $this->tableFields, array ('ID_template' => $this->templateId, 'ID_block' => $this->blockId ) );
		return $retValue;
	}
	
	/**
	 * Получает условия для WHERE для стандартных действий
	 * 
	 * @return mixed - false, если недостаточно данных / String, если все ок
	 */
	protected function _getKeysConditions() {
	  Service_Debug::setDebug();
		$conditions = '';
		$i = 0;
  	foreach ($this->arrPkNames as $fieldName => $attrName) {
  		if (!$this->$attrName) {
  			return false;
  		}
  		if ($i++) $conditions .= " AND ";
  		$conditions .= " `" . $this->tableName . "`.`" . $fieldName ."` = '" . $this->$attrName  . "' ";
  		$this->$attrName = $this->getField($fieldName);
  	}
  	return $conditions;
	}
	
	/**
	 * Получает условия для WHERE для таблицы
	 * 
	 * @return mixed - false, если недостаточно данных / String, если все ок
	 */
	public function getKeysConditions($tableName) {
	  Service_Debug::setDebug();
		$conditions = '';
		$i = 0;
  	foreach ($this->arrPkNames as $fieldName => $attrName) {
  		if (!$this->$attrName) {
  			return false;
  		}
  		if ($i++) $conditions .= " AND ";
  		$conditions .= " `" . $tableName . "`.`" . $fieldName ."` = '" . $this->$attrName  . "' ";
  	}
  	return $conditions;
	}	
	
  /**
   * Метод обновляет одно конкретное поле у таблицы
   *
   * @param string $fieldName - название поля в таблице
   * @param string $fieldValue - новое значение
   * @return bool
   */
  public function updateField($fieldName, $fieldValue) {
		Service_Debug::setDebug();
		$conditions = $this->_getKeysConditions();
		if (!$conditions) return false;
		$objDB = Service_Database::instance();
		$stmt = "UPDATE `" . $this->tableName . "`
		SET `" . $this->tableName . "`.`" . $fieldName . "` = '" . Service_Escape::absolutelyEscapeString($fieldValue) . "'
		WHERE " . $conditions;
		return $objDB->query($stmt);
  }
  
	/**
	 * Удаление объекта
	 * 
	 */
	public function delete() {
		Service_Debug::setDebug();
		$conditions = $this->_getKeysConditions();
		if (!$conditions) return false;
		$objDB = Service_Database::instance (); //Получаем подключение к БД
		$stmt = "DELETE FROM `" . $this->tableName . "` WHERE " . $conditions;
		return $objDB->query($stmt);
	}	
	
  /**
   * Возвращает имя таблицы
   */
  public function getTableName() {
    return $this->tableName;
  }
  
  /**
   * Метод находит в БД объект/объекты
   *
   * @param string $itemsToFind - тип поиска - число (id)/all/first
   * @param array $arrSettings - свойства иска
   * @return array
   */
  public function find($itemsToFind, $arrSettings = array()) {
  	Service_Debug::setDebug();
		// устанавливаем умолчания на случай, если через этот объект уже осуществляли поиск
    $this->limit = null;
		$this->offset = 0;
		$this->conditions = null;

		// Устанавливаем все поисковые свойства
		foreach ($arrSettings as $settingName => $settingValue) {
			$this->setSetting($settingName, $settingValue);
  	}

  	//определяем тип поиска и делаем соответствующий вызов
		if (is_integer($itemsToFind)) return $this->_findOne($itemsToFind);
		elseif ($itemsToFind == 'all') return $this->_findAll();
		elseif ($itemsToFind == 'first') return $this->_findFirst();
		else throw new Exception('The first parameter to the find method should be either an identifier or "all", or "first". You gave "' . $itemsToFind . '"');
	}

	/**
	 * Метод устанавливает свойство (поисковое)
	 *
	 * @param string $settingName
	 * @param mixed $settingValue
	 */
	public function setSetting($settingName, $settingValue) {
		switch ($settingName) {
			case 'select':
				$this->setSelect($settingValue);
				break;
			case 'conditions':
				$this->setConditions($settingValue);
				break;
			case 'order':
				$this->setOrder($settingValue);
				break;
		  case 'group':
				$this->setGroup($settingValue);
				break;
			case 'limit':
				$this->setLimit($settingValue);
				break;
			case 'offset':
				$this->setOffset($settingValue);
				break;
			case 'include':
				$this->setInclude($settingValue);
				break;
			case 'includeList':
			  $this->setIncludeList($settingValue);
				break;
			case 'combine':
				$this->setCombine($settingValue);
				break;
			case 'joins':
				$this->setJoins($settingValue);
				break;
			case 'author':
				$this->getAuthor = true;
				break;
			case 'id':
				$this->id = $settingValue;
				break;
			default:
				throw new Exception('You are trying to use invalid configuration ('. $settingName . '). The assume settings are: "select", "conditions", "joins", "include", "includeList", "order", "limit", "offset"');
				break;
		}
	}	
	
	/**
	 * Устанавливает свойство поиска SELECT
	 *
	 * @param string $select
	 */
	private function setSelect($select) {
		$this->select = $select;
	}
	/**
	 * Устанавливает свойство поиска WHERE
	 *
	 * @param string $conditions
	 */
	private function setConditions($conditions) {
		$this->conditions = $conditions;
	}
	/**
	 * Устанавливает свойство поиска ORDER
	 *
	 * @param string $order
	 */
	private function setOrder($order) {
		$this->order = $order;
	}
	/**
	 * Устанавливает свойство поиска GROUP
	 *
	 * @param string $group
	 */
	private function setGroup($group) {
		$this->group = $group;
	}
	/**
	 * Устанавливает ограничение на количество элементов в поиске
	 *
	 * @param string $limit
	 */
	private function setLimit($limit) {
		$this->limit = intval($limit);
	}
	/**
	 * Устанавливает начальную позицию поиска
	 *
	 * @param string $offset
	 */
	private function setOffset($offset) {
		$this->offset = intval($offset);
	}
	/**
	 * Устанавливает какие связи нужно задействовать в запросе дополнительно
	 *
	 * @param string $include
	 */
	private function setInclude($include) {
		$this->include = $include;
	}
	/**
	 * Устанавливает какбразом нужно скомбинировать результаты
	 *
	 * @param string $combine
	 */
	public function setCombine($combine) {
		$this->combine = $combine;
	}
	/**
	 * Устанавливает какие таблицы нужно соединить
	 *
	 * @param string $join
	 */
	public function setJoins($join) {
		$this->joins = $join;
	}
	

	/**
	 * Метод поиска первого элемента в списке
	 *
	 * @return array
	 */
	private function _findFirst() {
	  Service_Debug::setDebug();
	  $objDB = Service_Database::instance();// Получаем подключение к БД
	  // устанавливаем умолчания на случай, если через этот объект уже осуществляли поиск
		$this->limit = 1;
	  $this->offset = 0;

	  // осуществляем поиск и сохранение результатов
	  $this->_makeStmt();
	  $this->tableFields = $objDB->selectOne($this->stmt);
	  $this->id = $this->getField($this->pkName);
	  $this->loaded = true;

	  // Возвращаем сформированный результат
	  return $this->tableFields;
	}

	/**
	 * Переводим массив подключаемых полей, к многомерному виду
	 *
	 */
	private function convertCombineToAbsArr() {
		// проходим по всему массиву $this->combine (т.е. проходим по подключаемым таблицам)
		foreach ($this->combine as $combineName => $combine) {
			$arrCombine = explode(',', $combine); // Получаем набор полей для каждой из подключаемых таблиц
			// Проходим по всем полям в каждой подключаемой таблице
			foreach ($arrCombine as $fieldName) {
		    $fieldName  = trim($fieldName, ' `');
		    $this->combineConverted[$combineName][] = $fieldName; // Записываем в многомерный массив-комбайн/ Будет выглядеть как combineConverted['connected_table']['field']
			}
		}
	}
	
	/**
	 * Метод поиска списка элементов
	 *
	 * @return array
	 */
	private function _findAll() {
	  Service_Debug::setDebug();
		$objDB = Service_Database::instance();// Получаем подключение к БД
		$this->_makeStmt();
		Service_Debug::log(get_class($this) . ' stmt', $this->stmt);
		if (!$this->combine) { // Если обычная выборка
			return $objDB->select($this->stmt);
	  }
	  else { // Если комбинированная выборка, т.е. с выборкой из связных таблиц
	  	$arr = $objDB->select($this->stmt);	// Получаем данные об основной сущности вместе с данными о сопутствующих элементах
	  	$this->convertCombineToAbsArr(); 		// Переводим настройки комбайна в двумерный массив
	  	$i = 0;
	  	$arrRet = array();
	  	$arrLink = array();
	  	$pkName = $this->pkName;
	  	// Проходим по всем элементам основной сущности
	  	if ($arr) foreach ($arr as $row) {
	  		$id = $row[$pkName];
	  		if (!isset($arrLink[$id])) { // Если элемент встречается впервые
	  		  //Проходим по всем подключаемым таблицам (combineConverted['connected_table']['field'])
	  			foreach ($this->combineConverted as $combineName => $arrCombine) {
	  				if ($combineName != $this->tableName) {//Если это именно подключаемая таблица, а не собственная, тогда сохраняем все поля, относящиеся к этой таблице в отдельный массив
		  		  	
	  					//Проходим по всем полям подключаемой таблице
	  					foreach ($arrCombine as $fieldName) {
	  						$arrAdditional[$fieldName] = $row[$fieldName]; //Формируем подмассив для сохранения
	  					}

		  		  	$arrRet[$i][$combineName][] = $arrAdditional;  //Сохраняем подмассив
	  		  	}
	  		  	else{ // Если это собственная таблица, то прото переписываем все поля, относящиеся к этой таблице
	  		  		foreach ($arrCombine as $fieldName)	{$arrRet[$i][$fieldName] = $row[$fieldName];}
	  		  	}
	  		  }
	  		  $arrLink[$id] = $i; // Записываем ссылку на место, где сохранен соответствующий айдишник
	  		  $i++;
	  		}
	  		else { // Если элемент уже встречался
  			  $iTo = $arrLink[$id]; // Определяем, куда нужно сохранить элемент
	  			//Проходим по всем подключаемым таблицам
  			  foreach ($this->combineConverted as $combineName => $arrCombine) {
	  				if ($combineName != $this->tableName) {//Если это именно подключаемая таблица, а не собственная
		  		  	//Проходим по всем полям подключаемой таблице
		  		  	foreach ($arrCombine as $fieldName)	$arrAdditional[$fieldName] = $row[$fieldName]; //Формируем подмассив для сохранения
		  		  	$arrRet[$iTo][$combineName][] = $arrAdditional;  //Сохраняем подмассив
	  		  	}
	  			}
	  		}
	  	}
	  	return 	$arrRet;
	  }
	}
	
	/**
	 * Метод формирования поисковой строки
	 *
	 */
	private function _makeStmt() {
		$this->stmt = "SELECT " . (($this->combine && ($this->select == '*')) ? $this->_makeCombineSelect() : ($this->select == '*' ? '`' . $this->tableName . '`.*' : $this->select )) .
						($this->getAuthor ? ", `user`.`ID_user`, `is_admin`, `user_role`, `first_name`, `last_name`, `middle_name`, `login`, `user`.`pic_folder`, `user`.`pic_file`" : null) .
		        " FROM `" . $this->tableName . "`" .
		        ($this->joins ? (" " . $this->joins) : null) .
		        ($this->getAuthor ? " LEFT JOIN `user` ON `user`.`ID_user` = `" . $this->tableName . "`.`ID_user`" : null) .
		        ($this->conditions ? " WHERE " . $this->conditions : null) .
		        ($this->group ? " GROUP BY " . $this->group : null) .
		        ($this->order ? " ORDER BY " . $this->order : null) .
		        (isset($this->limit) ? " LIMIT " . $this->offset . ", " . $this->limit : null);
	}	
	
	
	/**
	 * Метод формирования селекта из комбайна
	 *
	 */
	private function _makeCombineSelect() {
		$retStr = $this->tableName . '.*, ';
		if (isset($this->combine)) {
			foreach ($this->combine as $table => $fields) {
				$arrFields = explode(',', $fields);
				foreach ($arrFields as $field) {
					$retStr .= "`" . $table . '`.' . $field . ', ';
				}
			}
		}
		else return $retStr;
		return substr($retStr, 0, strlen($retStr) - 2);
	}	
}
?>